// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
/* eslint-disable react-hooks/rules-of-hooks */

import formatMessage from 'format-message';
import { CallbackInterface, useRecoilCallback } from 'recoil';
import { defaultPublishConfig, isSkillHostUpdateRequired, PublishResult, PublishTarget } from '@bfc/shared';

import {
  publishTypesState,
  botStatusState,
  publishHistoryState,
  botBuildTimeErrorState,
  filePersistenceState,
  settingsState,
  runtimeStandardOutputDataState,
  botProjectFileState,
} from '../atoms/botState';
import { openInEmulator } from '../../utils/navigation';
import { botEndpointsState } from '../atoms';
import {
  rootBotProjectIdSelector,
  dialogsWithLuProviderSelectorFamily,
  luFilesSelectorFamily,
  qnaFilesSelectorFamily,
} from '../selectors';
import * as luUtil from '../../utils/luUtil';
import * as qnaUtil from '../../utils/qnaUtil';
import { ClientStorage } from '../../utils/storage';
import { RuntimeOutputData } from '../types';
import { checkIfFunctionsMissing, missingFunctionsError } from '../../utils/runtimeErrors';
import TelemetryClient from '../../telemetry/TelemetryClient';
import { TunnelingSetupNotification } from '../../components/Notifications/TunnelingSetupNotification';

import { BotStatus, Text, defaultBotEndpoint, defaultBotPort } from './../../constants';
import httpClient from './../../utils/httpUtil';
import { logMessage, setError } from './shared';
import { setRootBotSettingState } from './setting';
import { createNotification, addNotificationInternal } from './notification';

const PUBLISH_SUCCESS = 200;
const PUBLISH_PENDING = 202;
const PUBLISH_FAILED = 500;

export const publishStorage = new ClientStorage(window.sessionStorage, 'publish');

export const publisherDispatcher = () => {
  const publishFailure = async ({ set }: CallbackInterface, title: string, error, target, projectId: string) => {
    TelemetryClient.track('PublishFailure', { message: typeof error === 'string' ? error : JSON.stringify(error) });
    if (target.name === defaultPublishConfig.name) {
      set(botStatusState(projectId), BotStatus.failed);
      set(botBuildTimeErrorState(projectId), { ...error, title });
    }
    // prepend the latest publish results to the history

    set(publishHistoryState(projectId), (publishHistory) => {
      const targetHistory = publishHistory[target.name] ?? [];
      return {
        ...publishHistory,
        [target.name]: [error, ...targetHistory],
      };
    });
  };

  const publishSuccess = async ({ set }: CallbackInterface, projectId: string, data: PublishResult, target) => {
    TelemetryClient.track('PublishSuccess');
    const { endpointURL, status, port } = data;
    if (target.name === defaultPublishConfig.name) {
      if (status === PUBLISH_SUCCESS && endpointURL) {
        set(botStatusState(projectId), BotStatus.connected);
        set(botEndpointsState, (botEndpoints) => ({
          ...botEndpoints,
          [projectId]: { url: `${endpointURL}/api/messages`, port: port || defaultBotPort },
        }));
      } else {
        set(botStatusState(projectId), BotStatus.starting);
      }
    }

    set(publishHistoryState(projectId), (publishHistory) => {
      const targetHistory = publishHistory[target.name] ?? [];
      return {
        ...publishHistory,
        [target.name]: [
          {
            ...data,
            target: target,
          },
          ...targetHistory,
        ],
      };
    });
  };

  const updatePublishStatus = async (
    callbackHelpers: CallbackInterface,
    projectId: string,
    target: any,
    data: PublishResult,
  ) => {
    if (data == null) return;
    const { set, snapshot } = callbackHelpers;
    const { endpointURL, status, port } = data;

    // the action below only applies to when a bot is being started using the "start bot" button
    // a check should be added to this that ensures this ONLY applies to the "default" profile.
    if (target.name === defaultPublishConfig.name) {
      if (status === PUBLISH_SUCCESS && endpointURL) {
        const rootBotId = await snapshot.getPromise(rootBotProjectIdSelector);
        if (rootBotId === projectId) {
          // Update the skill host endpoint
          const settings = await snapshot.getPromise(settingsState(projectId));
          if (isSkillHostUpdateRequired(settings?.skillHostEndpoint)) {
            // Update skillhost endpoint only if ngrok url not set meaning empty or localhost url
            const updatedSettings = {
              ...settings,
              skillHostEndpoint: endpointURL + '/api/skills',
            };
            setRootBotSettingState(callbackHelpers, projectId, updatedSettings);
          }

          // display a notification for bots with remote skills the first time they are published
          // for a given session.
          const rootBotProjectFile = await snapshot.getPromise(botProjectFileState(rootBotId));
          const notificationCache = publishStorage.get('notifications') || {};
          if (
            !notificationCache[rootBotId] &&
            Object.values(rootBotProjectFile?.content?.skills ?? []).some((s) => s.remote)
          ) {
            const notification = createNotification({
              type: 'info',
              title: formatMessage('Setup tunneling software to test your remote skill'),
              onRenderCardContent: TunnelingSetupNotification,
              data: {
                port,
              },
            });
            addNotificationInternal(callbackHelpers, notification);
            publishStorage.set('notifications', {
              ...notificationCache,
              [rootBotId]: true,
            });
          }
        }
        set(botStatusState(projectId), BotStatus.connected);
        set(botEndpointsState, (botEndpoints) => ({
          ...botEndpoints,
          [projectId]: { url: `${endpointURL}/api/messages`, port: port || defaultBotPort },
        }));
      } else if (status === PUBLISH_PENDING) {
        set(botStatusState(projectId), BotStatus.starting);
      } else if (status === PUBLISH_FAILED) {
        set(botStatusState(projectId), BotStatus.failed);
        if (checkIfFunctionsMissing(data)) {
          set(botBuildTimeErrorState(projectId), {
            ...missingFunctionsError,
            title: formatMessage('Error occurred building the bot'),
          });
        } else {
          set(botBuildTimeErrorState(projectId), { ...data, title: formatMessage('Error occurred building the bot') });
        }
      }
    }

    if (status !== 404) {
      set(publishHistoryState(projectId), (publishHistory) => {
        const currentHistory = { ...data, target: target };
        let targetHistories = publishHistory[target.name] ? [...publishHistory[target.name]] : [];
        // if no history exists, create one with the latest status
        // otherwise, replace the latest publish history with this one
        if (targetHistories.length === 0) {
          targetHistories = [currentHistory];
        } else {
          // make sure this status payload represents the same item as item 0 (most of the time)
          // otherwise, prepend it to the list to indicate a NEW publish has occurred since last loading history
          if (targetHistories.length && targetHistories[0].id === data.id) {
            targetHistories[0] = currentHistory;
          } else {
            targetHistories.unshift(currentHistory);
          }
        }
        return { ...publishHistory, [target.name]: targetHistories };
      });
    }
  };

  const getPublishTargetTypes = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => {
    const { set } = callbackHelpers;
    try {
      const response = await httpClient.get(`/publish/types`);
      set(publishTypesState(projectId), response.data);
    } catch (err) {
      //TODO: error
      logMessage(callbackHelpers, err.message);
    }
  });

  const publishToTarget = useRecoilCallback(
    (callbackHelpers: CallbackInterface) =>
      async (projectId: string, target: PublishTarget, metadata: any, sensitiveSettings, token = '') => {
        try {
          const { snapshot } = callbackHelpers;
          const dialogs = await snapshot.getPromise(dialogsWithLuProviderSelectorFamily(projectId));
          const luFiles = await snapshot.getPromise(luFilesSelectorFamily(projectId));
          const qnaFiles = await snapshot.getPromise(qnaFilesSelectorFamily(projectId));
          const referredLuFiles = luUtil.checkLuisBuild(luFiles, dialogs);
          const referredQnaFiles = qnaUtil.checkQnaBuild(qnaFiles, dialogs);
          const response = await httpClient.post(`/publish/${projectId}/publish/${target.name}`, {
            publishTarget: target,
            accessToken: token,
            metadata: {
              ...metadata,
              luResources: referredLuFiles.map((file) => ({ id: file.id, isEmpty: file.empty })),
              qnaResources: referredQnaFiles.map((file) => ({ id: file.id, isEmpty: file.empty })),
            },
            sensitiveSettings,
          });

          // add job id to storage
          const publishJobIds = publishStorage.get('jobIds') || {};
          publishJobIds[`${projectId}-${target.name}`] = response.data.id;
          publishStorage.set('jobIds', publishJobIds);

          await publishSuccess(callbackHelpers, projectId, response.data, target);
        } catch (err) {
          // special case to handle dotnet issues
          await publishFailure(callbackHelpers, Text.CONNECTBOTFAILURE, err.response?.data, target, projectId);
        }
      },
  );

  const rollbackToVersion = useRecoilCallback(
    (callbackHelpers: CallbackInterface) => async (projectId: string, target: any, version, sensitiveSettings) => {
      try {
        const response = await httpClient.post(`/publish/${projectId}/rollback/${target.name}`, {
          version,
          sensitiveSettings,
        });
        await publishSuccess(callbackHelpers, projectId, response.data, target);
      } catch (err) {
        await publishFailure(callbackHelpers, Text.CONNECTBOTFAILURE, err.response.data, target, projectId);
      }
    },
  );

  // get bot status from target publisher
  const getPublishStatusV2 = useRecoilCallback(
    (callbackHelpers: CallbackInterface) => async (projectId: string, target: any, response: any) => {
      updatePublishStatus(callbackHelpers, projectId, target, response?.data);
    },
  );

  // get bot status from target publisher
  const getPublishStatus = useRecoilCallback(
    (callbackHelpers: CallbackInterface) => async (projectId: string, target: any, jobId?: string) => {
      try {
        const currentJobId =
          jobId ??
          (publishStorage.get('jobIds') ? publishStorage.get('jobIds')[`${projectId}-${target.name}`] : undefined);
        const response = await httpClient.get(
          `/publish/${projectId}/status/${target.name}${currentJobId ? '/' + currentJobId : ''}`,
        );

        updatePublishStatus(callbackHelpers, projectId, target, response.data);
      } catch (err) {
        updatePublishStatus(callbackHelpers, projectId, target, err.response?.data);
      }
    },
  );
  const getPublishHistory = useRecoilCallback(
    (callbackHelpers: CallbackInterface) => async (projectId: string, target: any) => {
      const { set, snapshot } = callbackHelpers;
      try {
        const filePersistence = await snapshot.getPromise(filePersistenceState(projectId));
        await filePersistence.flush();
        const response = await httpClient.get(`/publish/${projectId}/history/${target.name}`);
        set(publishHistoryState(projectId), (publishHistory) => ({
          ...publishHistory,
          [target.name]: response.data,
        }));
      } catch (err) {
        //TODO: error
        logMessage(callbackHelpers, err.response?.data?.message || err.message);
      }
    },
  );

  // only support local publish
  const stopPublishBot = useRecoilCallback(
    (callbackHelpers: CallbackInterface) =>
      async (projectId: string, target: any = defaultPublishConfig) => {
        const { set, snapshot } = callbackHelpers;
        try {
          const currentBotStatus = await snapshot.getPromise(botStatusState(projectId));
          // Change to "Stopping" status only if the Bot is not in a failed state or inactive state
          if (currentBotStatus !== BotStatus.failed && currentBotStatus !== BotStatus.inactive) {
            set(botStatusState(projectId), BotStatus.stopping);
          }

          await httpClient.post(`/publish/${projectId}/stopPublish/${target.name}`);

          if (currentBotStatus !== BotStatus.failed) {
            set(botStatusState(projectId), BotStatus.inactive);
          }
        } catch (err) {
          setError(callbackHelpers, err);
          logMessage(callbackHelpers, err.message);
        }
      },
  );

  const resetBotRuntimeLog = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => {
    const { reset } = callbackHelpers;
    reset(botBuildTimeErrorState(projectId));
    reset(runtimeStandardOutputDataState(projectId));
  });

  const openBotInEmulator = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => {
    const { snapshot } = callbackHelpers;
    const botEndpoints = await snapshot.getPromise(botEndpointsState);
    const settings = await snapshot.getPromise(settingsState(projectId));
    try {
      openInEmulator(
        botEndpoints[projectId]?.url || defaultBotEndpoint,
        settings.MicrosoftAppId && settings.MicrosoftAppPassword
          ? { MicrosoftAppId: settings.MicrosoftAppId, MicrosoftAppPassword: settings.MicrosoftAppPassword }
          : { MicrosoftAppPassword: '', MicrosoftAppId: '' },
      );
    } catch (err) {
      setError(callbackHelpers, err);
      logMessage(callbackHelpers, err.message);
    }
  });

  const setRuntimeStandardOutputData = useRecoilCallback(
    (callbackHelpers: CallbackInterface) => async (projectId: string, data: RuntimeOutputData) => {
      const { set } = callbackHelpers;
      try {
        set(runtimeStandardOutputDataState(projectId), data);
      } catch (err) {
        setError(callbackHelpers, err);
        logMessage(callbackHelpers, err.message);
      }
    },
  );

  return {
    getPublishTargetTypes,
    publishToTarget,
    stopPublishBot,
    rollbackToVersion,
    getPublishStatus,
    getPublishStatusV2,
    getPublishHistory,
    openBotInEmulator,
    resetBotRuntimeLog,
    setRuntimeStandardOutputData,
  };
};
